#region Using

using System.Collections;
using System.Collections.Generic;

using Common.Base;

using GameCommon.HexagonBoard.Enums;
using GameCommon.HexagonBoard.Exceptions;

#endregion

namespace GameCommon.HexagonBoard
{
    public class HexagonBoard<T> : IEnumerable<T> where T : new()
    {
        public delegate bool StructurizerDelegate(Size boardDimension, Position index);

        /// <summary>
        /// Responsible for deciding wether a tile is an active part of the board.
        /// </summary>
        private readonly StructurizerDelegate structurizer;

        /// <summary>
        /// Tile matrix that forms the board.
        /// </summary>
        protected readonly BoardMember[,] tiles;

        public HexagonBoard(Size dimension)
            : this(dimension, Topology.Default, null)
        {
        }

        public HexagonBoard(Size dimension, Topology topology)
            : this(dimension, topology, null)
        {
        }

        public HexagonBoard(Size dimension, StructurizerDelegate structurizer)
            : this(dimension, Topology.Customized, structurizer)
        {
        }

        private HexagonBoard(Size dimension, Topology topology, StructurizerDelegate structurizer)
        {
            this.Dimension = dimension;
            this.Topology = topology;

            switch (this.Topology)
            {
                case Topology.Hexagon:

                    // Dimension must be odd and symmetric to form a hexagon.
                    if (this.Dimension.Height % 2 == 0)
                    {
                        this.Dimension.Height += 1;
                        this.Dimension.Width = this.Dimension.Height;
                    }

                    this.structurizer = this.HexagonStructurizer;
                    this.Dimension.Width = this.Dimension.Height;
                    break;

                case Topology.Triangle:
                    this.structurizer = this.TriangleStructurizer;
                    this.Dimension.Width = this.Dimension.Height;
                    break;

                case Topology.Default:
                    this.structurizer = this.DefaultStructurizer;
                    break;

                case Topology.Customized:
                    this.structurizer = structurizer ?? this.DefaultStructurizer;
                    break;
            }

            // Y-axis flipped and rotated to match the easy hex-system
            // hence, Y depends on X
            var maxIndexY = this.Dimension.Height + (this.Dimension.Width / 2);

            this.tiles = new BoardMember[this.Dimension.Width, maxIndexY];

            for (var i = 0; i < this.Dimension.Width; i++)
            {
                for (var j = 0; j < maxIndexY; j++)
                {
                    var active = this.structurizer(this.Dimension, new Position(i, j));
                    this.tiles[i, j] = new BoardMember(new Position(i, j), active);
                }
            }
        }

        /// <summary>
        /// Actual width und heigth of the board.
        /// </summary>
        public Size Dimension { get; private set; }

        /// <summary>
        /// Defines the structure of the board.
        /// </summary>
        public Topology Topology { get; private set; }

        public IEnumerator<T> GetEnumerator()
        {
            foreach (var member in this.tiles)
            {
                if (member.Active)
                {
                    yield return member.HexTile;
                }
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        /// <summary>
        /// Gets all six surrounding neighbors of a tile, 
        /// or less if it is not fully surrounded.
        /// </summary>
        /// <param name="tile">The center tile to get all neighbors for.</param>
        /// <returns>
        /// List of all neighbors, started from northern direction going counter-clockwise.
        /// </returns>
        public IEnumerable<T> GetNeighbors(T tile)
        {
            BoardMember center = null;

            // Find the matching wrapper object
            foreach (var member in this.tiles)
            {
                if (tile.Equals(member.HexTile))
                {
                    center = member;
                    break;
                }
            }

            if (center == null)
            {
                throw new InvalidMemberException("Tile is not a member of the board.");
            }

            var neighbors = new List<T>();
            var indices = new List<Position> { // North
                                                 new Position(center.Index.X, center.Index.Y + 1), 
                                    
                                                 // North-West
                                                 new Position(center.Index.X - 1, center.Index.Y), 
                                    
                                                 // South-West
                                                 new Position(center.Index.X - 1, center.Index.Y - 1), 
                                    
                                                 // South
                                                 new Position(center.Index.X, center.Index.Y - 1), 
                                    
                                                 // South-East
                                                 new Position(center.Index.X + 1, center.Index.Y), 
                                    
                                                 // North-East
                                                 new Position(center.Index.X + 1, center.Index.Y + 1) };

            foreach (var index in indices)
            {
                if (this.IndexValidation(index))
                {
                    neighbors.Add(this.tiles[index.X, index.Y].HexTile);
                }
            }

            return neighbors;
        }

        /// <summary>
        /// Gets a tile according to an index.
        /// </summary>
        /// <param name="index">The index of the requested tile.</param>
        /// <returns>The tile with the stated position within the board.</returns>
        public T GetTile(Position index)
        {
            if (this.IndexValidation(index))
            {
                return this.tiles[index.X, index.Y].HexTile;
            }

            throw new InvalidMemberException("Position has no tile");
        }

        /// <summary>
        /// Gets the index for a tile.
        /// </summary>
        /// <param name="tile">The tile with the requested index.</param>
        /// <returns>The index of the stated tile.</returns>
        public Position GetIndex(T tile)
        {
            foreach (var member in this.tiles)
            {
                if (tile.Equals(member.HexTile))
                {
                    return member.Index;
                }
            }

            throw new InvalidMemberException("Tile is not a member of the board");
        }

        /// <summary>
        /// Verifies if an index is valid.
        /// </summary>
        /// <param name="index">The index to verify</param>
        /// <returns>True, if the index is valid.</returns>
        private bool IndexValidation(Position index)
        {
            if (index.X >= 0 && index.X < this.Dimension.Width && index.Y >= 0 && index.Y < this.Dimension.Height + (this.Dimension.Width / 2))
            {
                return this.tiles[index.X, index.Y].Active;
            }

            return false;
        }

        /// <summary>
        /// Decides if an index is an active part of a rectangular shaped board.
        /// </summary>
        /// <param name="boardDimension">The size of the board.</param>
        /// <param name="index">The index to make the decision for.</param>
        /// <returns>True, if the index is an active part of the board.</returns>
        private bool DefaultStructurizer(Size boardDimension, Position index)
        {
            return index.Y >= (index.X / 2) && index.Y < boardDimension.Height + (index.X / 2);
        }

        /// <summary>
        /// Decides if an index is an active part of a board that forms a conventional hexagon.
        /// </summary>
        /// <param name="boardDimension">The size of the hexagon.</param>
        /// <param name="index">The index to make the decision for.</param>
        /// <returns>True, if the index is an active part of the board.</returns>
        private bool HexagonStructurizer(Size boardDimension, Position index)
        {
            // The hexagon will depend on the height-value of the given dimension
            var height = boardDimension.Height % 2 == 0 ? boardDimension.Height + 1 : boardDimension.Height;
            var edge = 1 + (height / 2);
            var shift = 0;

            if (edge % 2 == 0)
            {
                shift = 1;
            }

            if (index.X < edge && index.X >= 0)
            {
                // Left board half and not out of border
                if (index.Y >= (edge / 2) - shift && index.Y <= (height - 1) - ((edge - 1) / 2 - index.X + shift))
                {
                    return true;
                }
            }
            else if (index.X >= edge && index.X <= height - 1)
            {
                // Right board half and not out of border
                if (index.Y >= (height / 4) + (index.X + 1 - edge) && index.Y <= (height - 1) + ((edge - 1) / 2))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Decides if an index is an active part of a board that forms a triangle.
        /// </summary>
        /// <param name="boardDimension">The size of the Triangle. (equilateral)</param>
        /// <param name="index">The index to make the decision for.</param>
        /// <returns>True, if the index is an active part of the board.</returns>
        private bool TriangleStructurizer(Size boardDimension, Position index)
        {
            return index.Y <= boardDimension.Height - 1 && index.Y >= index.X;
        }

        /// <summary>
        /// Wrapper for advanced tile management.
        /// </summary>
        protected class BoardMember
        {
            public BoardMember(Position index, bool isActive)
            {
                this.HexTile = new T();
                this.Index = index;
                this.Active = isActive;
            }

            /// <summary>
            /// Represents a single hexagon field on the board.
            /// </summary>
            public T HexTile { get; private set; }

            /// <summary>
            /// The board tile index.
            /// </summary>
            public Position Index { get; private set; }

            /// <summary>
            /// Is true, if this member is an active part of the board.
            /// </summary>
            public bool Active { get; private set; }
        }
    }
}